﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using GA.Server.Api;
using GA.Server.Configuration;
using System.Threading;
using log4net;
using System.Data.SqlClient;

namespace GA.Server.DatabaseManager
{
    public class DatabaseManagerServer : GAServer
    {
        /// <summary>
        /// Gets the name of the server.
        /// </summary>
        public override string Name
        {
            get { return GetType().Name; }
        }

        /// <summary>
        /// Gets the maximum total connections available for the database server.
        /// </summary>
        public int MaxConnections { get; private set; }

        /// <summary>
        /// Default constructor
        /// </summary>
        public DatabaseManagerServer()
        {
        }

        /// <summary>
        /// Constructor for building a server.
        /// </summary>
        /// <param name="properties">Properties to pass to the core.</param>
        /// <exception cref="ArgumentNullException">Thrown if properties is null.</exception>
        /// <exception cref="InvalidOperationException">Thrown if properties does not have adaquate server information.</exception>
        public DatabaseManagerServer(ServerPropertiesElementCollection properties)
            : base(properties)
        {
            if (properties == null) throw new ArgumentNullException("properties");
            if (properties.Get("Server") == null || properties.Get("Server").Value.Length == 0) throw new InvalidOperationException("Properties must have Server information.");
            if (properties.Get("DatabaseName") == null || properties.Get("DatabaseName").Value.Length == 0) throw new InvalidOperationException("Properties must have Database information.");
            if (properties.Get("UseAuthentication") != null && !bool.Parse(properties.Get("UseAuthentication").Value) && (properties.Get("Username") == null || properties.Get("Password") == null || properties.Get("Username").Value.Length == 0 || properties.Get("Password").Value.Length == 0)) throw new InvalidOperationException("If Authentication is not true, Username and Password must be specified.");
            mLog = LogManager.GetLogger("GA.Server.DatabaseManager");
            mLog.Info("Loading DatabaseManager...");


            //Settings
            MaxConnections = properties.Get("MaxConnections") == null ? -1 : int.Parse(properties.Get("MaxConnections").Value);
            if (MaxConnections > 0 && MaxConnections < 30)
            {
                throw new InvalidOperationException("DatabaseManager cannot start with less than 30 connections available.");
            }
            SqlConnectionStringBuilder connectionString = new SqlConnectionStringBuilder();
            connectionString.DataSource = properties.Get("Server").Value;
            connectionString.InitialCatalog = properties.Get("DatabaseName").Value;
            connectionString.IntegratedSecurity = properties.Get("UseAuthentication") == null ? true : bool.Parse(properties.Get("UseAuthentication").Value);
            if (!connectionString.IntegratedSecurity)
            {
                connectionString.UserID = properties.Get("Username").Value;
                connectionString.Password = properties.Get("Password").Value;
            }
            connectionString.ConnectTimeout = properties.Get("ConnectionTimeout") == null ? 30 : int.Parse(properties.Get("ConnectionTimeout").Value);
            mCachedConnectionString = connectionString.ToString();

            //Fill Pools
            mAvailablePools = new Dictionary<string, List<AutoResetEvent>>();
            Dictionary<string, int> mPoolSize = new Dictionary<string, int>();
            mPoolSize.Add("Default", 64);
            if (properties.Get("Pools") != null)
            {
                foreach (ServerPropertyElement property in properties.Get("Pools").ServerProperties.GetProperties())
                {
                    int poolSize = int.Parse(property.Value);
                    if (poolSize > 64)
                    {
                        poolSize = 64;
                        mLog.WarnFormat("Pool {0} has a pool size of {1}. Max pool size for any pool is 64.", property.Key, property.Value);
                    }
                    if (mPoolSize.ContainsKey(property.Key))
                    {
                        mPoolSize[property.Key] = poolSize;
                    }
                    else
                    {
                        mPoolSize.Add(property.Key, poolSize);
                    }
                }
            }
            foreach (string poolName in mPoolSize.Keys)
            {
                FillPool(poolName, mPoolSize[poolName]);
            }
            mTotalOpenConnections = 0;

            Status = GAServerStatus.Initialized;
        }

        /// <summary>
        /// Gets a connection.
        /// </summary>
        /// <param name="poolName">Pool name to pull from.</param>
        /// <param name="maxWaitTime">Maximum wait time to wait for a connection.</param>
        /// <param name="retryTime">Amount of time to wait for a connection per try.</param>
        /// <returns>KeyValuePair for a SqlConnection and AutoResetEvent.</returns>
        /// <remarks>AutoResetEvent should be set when the calling script is done with the SqlConnection.</remarks>
        /// <exception cref="ArgumentNullException">Thrown if maxWaittime or retryTime is null.</exception>
        /// <exception cref="InvalidOperationException">Thrown if the server has stopped.</exception>
        /// <exception cref="TimeoutException">Thrown if the time limit expired.</exception>
        public KeyValuePair<SqlConnection, AutoResetEvent> GetConnection(string poolName, TimeSpan maxWaitTime, TimeSpan retryTime)
        {
            if (maxWaitTime == null) throw new ArgumentNullException("maxWaitTime");
            if (retryTime == null) throw new ArgumentNullException("retryTime");

            if (poolName == null || poolName.Length == 0) poolName = "Default";
            if (!mAvailablePools.ContainsKey(poolName))
            {
                mLog.DebugFormat("Pool {0} does not exist. Using Default", poolName);
            }

            int resetEventId = WaitHandle.WaitTimeout;
            DateTime startWait = DateTime.Now;
            while (resetEventId == WaitHandle.WaitTimeout && DateTime.Now.Subtract(startWait) < maxWaitTime && !ForceStopProcess)
            {
                resetEventId = AutoResetEvent.WaitAny(mAvailablePools[poolName].ToArray(), retryTime);
            }
            if (ForceStopProcess)
            {
                throw new InvalidOperationException("ForceStopProcess called.");
            }
            if (resetEventId == WaitHandle.WaitTimeout)
            {
                throw new TimeoutException("Could not find a valid connection in the specified time limit.");
            }
            if (MaxConnections > 0)
            {
                lock (mLockObject)
                {
                    if (mTotalOpenConnections > MaxConnections)
                    {
                        while (mTotalOpenConnections > MaxConnections && DateTime.Now.Subtract(startWait) < maxWaitTime && !ForceStopProcess)
                        {
                            System.Threading.Thread.Sleep(50);
                        }
                    }
                    mTotalOpenConnections++;
                }
            }
            if (ForceStopProcess)
            {
                throw new InvalidOperationException("ForceStopProcess called.");
            }
            if (DateTime.Now.Subtract(startWait) >= maxWaitTime)
            {
                if (resetEventId != WaitHandle.WaitTimeout)
                {
                    mAvailablePools[poolName][resetEventId].Set();
                }
                throw new TimeoutException("Could not find a valid connection in the specified time limit.");
            }
            AutoResetEvent resetEvent = mAvailablePools[poolName][resetEventId];
            return new KeyValuePair<SqlConnection, AutoResetEvent>(new SqlConnection(mCachedConnectionString), resetEvent);
        }

        private Dictionary<string, List<AutoResetEvent>> mAvailablePools;
        private ILog mLog;
        private string mCachedConnectionString;
        private int mTotalOpenConnections;
        private object mLockObject = 0.998;

        private void FillPool(string poolName, int poolSize)
        {
            mAvailablePools.Add(poolName, new List<AutoResetEvent>());
            for (int i = 0; i < poolSize; i++)
            {
                mAvailablePools[poolName].Add(new AutoResetEvent(true));
            }
        }
    }
}
